﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SyncLib.Interface;
using log4net;
using Microsoft.Synchronization.Data;
using SyncLib.Data;
using System.Data.SqlClient;
using Microsoft.Synchronization.Data.SqlServer;
using System.Data;
using Microsoft.Synchronization;
using System.Reflection;
using SyncLib.Client;

namespace SyncLib.Provider
{
    public class BaseKeeperSync : IKeeperSync, ISyncSessionManager
    {
        #region Fields
        ILog Log = LogManager.GetLogger(typeof(Log4NetImplement));
        protected SyncContext _SyncContext;
        protected Guid SessionId;
        protected Dictionary<string, List<KSyncConflict>> ConflictDatas;
        protected DbSyncScopeProgress ScopeProcess;
        protected Dictionary<string, DbSyncScopeProgress> DependencyStatistic;
        protected ProxyInfo<ISyncManager> _ProxyInfo;
        protected SqlTransaction _Transaction;
        protected SqlSyncProvider _Provider;

        public Guid ClientId { get; set; }
        #endregion
        public BaseKeeperSync()
        {
            Log.Info("BaseKeeperSync Constructor");
            _SyncContext = new SyncContext();
            ConflictDatas = new Dictionary<string, List<KSyncConflict>>();
            DependencyStatistic = new Dictionary<string, DbSyncScopeProgress>();

        }
        ~BaseKeeperSync()
        {
            try
            {
                Log.Info("BaseKeeperSync Destructor");
                EndSync();
            }
            catch (Exception ex)
            {
                Log.Fatal("Destructor BaseKeeperSync Exception:", ex);
            }
        }
        #region IKeeperSync Members

        public virtual NextAction Initialize(SyncLib.Data.ScopeInfoEx scope, Microsoft.Synchronization.SyncProviderPosition position, out string message)
        {
            Log.InfoFormat(" BaseKeeperSync Initialize with ScopeExId={0} Position={1}",
                            scope.ScopeExId, position);

            message = "Unknown";
            _SyncContext.CurrentScope = scope;
            _SyncContext.Position = position;

            try
            {
                Log.Info("Try test ConnectionString(" + KeeperSyncUtility.GetSecuConnectionString() + ")");
                SqlHelper.ExecuteNonQuery(CommonVariable.SqlConnectionString, CommandType.Text, "Select 1");
                Log.Info("ConnectionString OK");
                return NextAction.Continue;
            }
            catch (Exception ex)
            {
                message = ex.Message;
            }
            return NextAction.Skip;
        }

        public virtual NextAction PrepareSync(out string message)
        {
            message = "Unknown";
            try
            {
                SqlConnection conn = new SqlConnection(CommonVariable.SqlConnectionString);
                ScopeInfoEx scope = _SyncContext.CurrentScope;
                Log.Info("Check Exists Scope");
                if (_SyncContext.Position == SyncProviderPosition.Local)
                    CheckScopeAtClient(conn, scope);
                else
                    CheckScopeAtRemote(conn, scope);
                Log.Info("Finish Check scope");
                _Provider = new SqlSyncProvider(scope.ScopeName, conn, "", scope.Schema);
                _Provider.MemoryDataCacheSize = (uint)scope.BatchSize;
                _Provider.BatchingDirectory = CommonVariable.SyncFolderBatch;
                _Provider.CleanupBatchingDirectory = true;
                AttachProviderEvent();
                return NextAction.Continue;
            }
            catch (Exception ex)
            {
                message = ex.Message;
            }
            return NextAction.Skip;
        }

        public virtual NextAction BeginSync(out string message)
        {
            message = "unknown";
            return NextAction.Continue;
        }

        public Microsoft.Synchronization.Data.RelationalSyncProvider Provider
        {
            get { return _Provider; }
        }

        public KSyncStatistics SyncStatistics
        {
            get
            {
                KSyncStatistics statics = new KSyncStatistics();
                statics.ScopeProcess = ScopeProcess ?? new DbSyncScopeProgress();
                statics.ConflictDatas = ConflictDatas;
                statics.DependencyStatistic = DependencyStatistic;
                statics.SessionId = SessionId.Equals(Guid.Empty) ? Guid.NewGuid() : SessionId;
                return statics;
            }
            set
            {
                throw new NotImplementedException();
            }
        }




        public SyncContext Context
        {
            get
            {
                return _SyncContext;
            }
            set
            {
                if (value == null)
                    Log.Info("WHAT'S THE HELL, SET CONTEXT NULL");
                _SyncContext = value;
            }
        }
        public ProxyInfo<ISyncManager> ProxyInfo
        {
            get { return _ProxyInfo; }
            set { _ProxyInfo = value; }
        }

        protected ISyncManager Proxy
        {
            get
            {
                if (_ProxyInfo == null)
                    return null;
                return _ProxyInfo.Channel;
            }

        }

        protected ISyncConfigManager ConfigProxy
        {
            get
            {
                if (_ProxyInfo == null)
                    return null;
                return _ProxyInfo.Channel;
            }

        }

        public virtual void EndSync()
        {
            _Transaction = null;

            if (ConflictDatas != null)
            {
                ConflictDatas.Clear();
                ConflictDatas = null;
            }
            _ProxyInfo = null;
            ScopeProcess = null;
            if (_Provider != null)
            {
                _Provider.Dispose();
                _Provider = null;
            }
        }
        public int Type { get; set; }
        public virtual void Synchronize()
        {

        }
        #endregion
        #region Custom Method

        protected KApplyAction TOSSolveConfig(DbApplyChangeFailedEventArgs e)
        {
            TableOfScope table = Context.CurrentScope.FindTable(e.Conflict.LocalChange.TableName);
            try
            {
                if (table != null)
                {
                    switch (table.SolveConfigType)
                    {
                        case KeeperSyncUtility.Solve_Auto:
                            return AutoSolveConfig(e);

                        case KeeperSyncUtility.Solve_CallProc:
                            Context.CurrentTable = table;
                            return KeeperSyncUtility.TOSSolveConfigCommand(e, this);

                        case KeeperSyncUtility.Solve_ClientWin:
                            if (_SyncContext.Position == SyncProviderPosition.Local)
                                return KApplyAction.Continue;
                            else
                                return KApplyAction.RetryWithForceWrite;

                        case KeeperSyncUtility.Solve_ServerWin:
                            if (_SyncContext.Position == SyncProviderPosition.Local)
                                return KApplyAction.RetryWithForceWrite;
                            else
                                return KApplyAction.Continue;
                        case KeeperSyncUtility.Solve_Manual:
                            return KApplyAction.UserManual;

                    }
                }


            }
            catch
            {
                throw;
            }
            finally
            {
                Context.CurrentTable = null;
            }
            return KApplyAction.Continue;
        }
        protected KApplyAction AutoSolveConfig(DbApplyChangeFailedEventArgs e)
        {
            DataTable tblLocal = e.Conflict.LocalChange;
            DataTable tblRemote = e.Conflict.RemoteChange;
            DataRow localRow = tblLocal.Rows[0];
            DataRow remoteRow = tblRemote.Rows[0];
            DateTime localDate = DateTime.MinValue, remoteDate = DateTime.MinValue;
            //if (tblLocal.Columns.Contains("last_change_datetime") && tblRemote.Columns.Contains("last_change_datetime"))
            //{
            //    if (!localRow.IsNull("last_change_datetime") && !remoteRow.IsNull("last_change_datetime"))
            //    {
            //        localDate = (DateTime)localRow["last_change_datetime"];
            //        remoteDate = (DateTime)remoteRow["last_change_datetime"];

            //    }
            //}
            if (localDate == DateTime.MinValue)
            {
                if (e.Conflict.LocalChange.Columns.Contains("time0") && e.Conflict.LocalChange.Columns.Contains("date0"))
                {
                    string timeSpan1 = localRow["time0"].ToString();
                    string timeSpan2 = remoteRow["time0"].ToString();
                    localDate = (DateTime)localRow["date0"];
                    remoteDate = (DateTime)remoteRow["date0"];

                    TimeSpan ts = TimeSpan.Parse(timeSpan1);
                    localDate = localDate.AddMilliseconds(ts.TotalMilliseconds);
                    ts = TimeSpan.Parse(timeSpan2);
                    remoteDate = remoteDate.AddMilliseconds(ts.TotalMilliseconds);
                }
            }
            if (localDate > remoteDate)
                return KApplyAction.Continue;
            else
                return KApplyAction.RetryWithForceWrite;
        }
        protected KApplyAction ConvertApplyActionByLocaltion(KApplyAction action)
        {
            if (_SyncContext.Position == SyncProviderPosition.Local)
                return action;
            else
                switch (action)
                {
                    case KApplyAction.Continue:
                        return action;
                    case KApplyAction.RetryApplyingRow:
                        return KApplyAction.RetryApplyingRow;
                }
            return action;
        }
        protected void RemoveConflictRowFromChange(DbChangesSelectedEventArgs e)
        {
            try
            {
                Log.InfoFormat("Begin RemoveConflictRowFromChange");
                DataSet ds = e.Context.DataSet;
                foreach (DataTable tbl in ds.Tables)
                {
                    Log.InfoFormat("    Table \"{0}\" RemoveConflictRowFromChange", tbl.TableName);
                    if (ConflictDatas.ContainsKey(tbl.TableName))
                    {
                        List<KSyncConflict> lstConflict = ConflictDatas[tbl.TableName];
                        TableOfScope tos = _SyncContext.CurrentScope.FindTable(tbl.TableName);
                        if (tos == null)
                        {
                            Log.WarnFormat("RemoveConflictRowFromChange[For-Continue]: TableOfScope Name ={0} not found in ScopeInfoEx", tbl.TableName);
                            continue;
                        }

                        for (int i = 0; i < lstConflict.Count; i++)
                        {
                            KSyncConflict node = lstConflict[i];
                            if (node.Action != KApplyAction.UserManual)
                                continue;

                            DataView view = new DataView(tbl);
                            view.RowStateFilter = DataViewRowState.Deleted | DataViewRowState.CurrentRows;
                            view.Sort = node.ViewSort;

                            DataRowView[] selectedRows = view.FindRows(node.KeyValues);
                            if (selectedRows.Length != 1)
                                throw new Exception(string.Format("Scope {0} Table {1} find with primary key but result {2}",
                                    _SyncContext.CurrentScope.ScopeName, tbl.TableName, selectedRows.Length));

                            StringBuilder sb = new StringBuilder();
                            for (int j = 0; j < node.KeyValues.Length; j++)
                            {
                                object obj = node.KeyValues[j];
                                sb.Append(obj);
                                if (j < node.KeyValues.Length - 1)
                                    sb.Append(",");
                            }

                            Log.InfoFormat("Delete row conflict from change. PrimaryColumn \"{0}\" Data {1}", tos.PrimaryKey, sb.ToString());
                            selectedRows[0].Row.Delete();
                            selectedRows[0].Row.AcceptChanges();
                        }
                    }
                }
                Log.Fatal("End RemoveConflictRowFromChange");
            }
            catch (Exception ex)
            {
                Log.Fatal("End RemoveConflictRowFromChange with Exception ", ex);
                throw ex;
            }
        }


        protected void PreApplyTableCommand(DataSet ds)
        {
            foreach (DataTable tbl in ds.Tables)
            {
                TableOfScope ttemp = _SyncContext.CurrentScope.FindTable(tbl.TableName);
                if (ttemp != null)
                {
                    if (string.IsNullOrEmpty(ttemp.PreApplyCommand)) continue;
                    _SyncContext.CurrentTable = ttemp;

                    DataView view = new DataView(tbl);
                    view.RowStateFilter = DataViewRowState.CurrentRows | DataViewRowState.Deleted;
                    KeeperSyncUtility.TOSPreApplyCommand(view, this);
                }
            }
            _SyncContext.CurrentTable = null;
        }
        protected void AfterApplyTableCommand(DataSet ds, DbSyncStage state)
        {
            foreach (DataTable tbl in ds.Tables)
            {
                TableOfScope ttemp = _SyncContext.CurrentScope.FindTable(tbl.TableName);
                if (ttemp != null)
                {
                    if (string.IsNullOrEmpty(ttemp.AfterApplyCommand)) continue;
                    _SyncContext.CurrentTable = ttemp;
                    DataView view = new DataView(tbl);
                    view.RowStateFilter = DataViewRowState.CurrentRows | DataViewRowState.Deleted;
                    KeeperSyncUtility.TOSAfterApplyCommand(view, state, this);
                }
            }
            _SyncContext.CurrentTable = null;
        }
        protected virtual void CheckScopeAtClient(SqlConnection conn, ScopeInfoEx scope)
        {
            if (!KeeperSyncUtility.ExistsScope(scope))
            {
                Log.InfoFormat("{1} not exists ScopeName [{0}] try get it from sever", scope.ScopeName, _SyncContext.Position);
                if (!this.ConfigProxy.ExistsScope(scope))
                {
                    throw new Exception("Not exists ScopeName [" + scope.ScopeName + "] on both client and server");

                }
                if (!string.IsNullOrEmpty(scope.TemplateName) && !KeeperSyncUtility.ExistsTemplate(scope))
                {
                    //Apply Template 
                    //MonitorEvent.Monitor.Info("Tạo mẫu phạm vi");
                    //DbSyncScopeDescription templateDess = ConfigProxy.GetTemplateDescription(scope);
                    //KeeperSyncUtility.CreateTemplateDescription(templateDess, scope);
                }
                Log.Info("  Try get scope from server");
                DbSyncScopeDescription scopeDess = ConfigProxy.GetScopeDescription(scope);
                Log.Info("  Try Apply scope to Client");
                KeeperSyncUtility.CreateScopeDescription(scopeDess, scope);
            }
            if (!ConfigProxy.ExistsScope(scope))
            {
                Log.InfoFormat("Server not exists ScopeName [{0}]. Try push it from client", scope.ScopeName);
                Log.Info("  Load DbSyncScopeDescription");
                DbSyncScopeDescription clientScopeDess = KeeperSyncUtility.GetScopeDescription(scope);
                Log.Info("  Push to server");
                ConfigProxy.CreateScopeDescription(clientScopeDess, scope);
            }
        }
        protected virtual void CheckScopeAtRemote(SqlConnection conn, ScopeInfoEx scope)
        {
            if (!KeeperSyncUtility.ExistsScope(scope))
            {
                Log.InfoFormat("{1} not exists ScopeName [{0}]", scope.ScopeName, _SyncContext.Position);

                throw new Exception("Not exists ScopeName [" + scope.ScopeName + "] on server");
            }
        }
        protected void ResetSomeClientConfig()
        {
            string sql = "Update [dbo].[ClientConfig] set [UploadFirst]=1, [DownloadFirst]=1 WHERE ClientId='{0}' AND ScopeExId='{1}'";
            sql = string.Format(sql, _SyncContext.CurrentScope.ScopeExId, _SyncContext.CurrentScope.ClientID);

            using (SqlCommand command = new SqlCommand(sql, Connection, Transaction))
            {
                command.ExecuteNonQuery();
            }

        }
        #endregion


        #region ISyncSessionManager Members

        public SqlTransaction Transaction
        {
            get { return _Transaction; }
            protected set { _Transaction = value; }
        }

        public SqlConnection Connection
        {
            get { return _Provider.Connection as SqlConnection; }
        }

        #endregion

        #region Virtual Operator

        protected virtual void OnApplyChangeFailed(DbApplyChangeFailedEventArgs e)
        {

            try
            {

                Log.InfoFormat("SessionId={0} {1} ApplyChangeFailed Conflict={2}",
                    e.Session.SessionId,
                    _SyncContext.CurrentScope.ScopeName,
                    e.Conflict.Type);
                if (e.Conflict.Type == DbConflictType.ErrorsOccurred)
                    throw new Exception("Có lỗi khi thực hiện đồng bộ " + e.Conflict.ErrorMessage);

                string tableName = e.Conflict.LocalChange != null ? e.Conflict.LocalChange.TableName : e.Conflict.RemoteChange.TableName;
                TableOfScope tos = _SyncContext.CurrentScope.FindTable(tableName);
                KApplyAction kaction = KApplyAction.Unknown;
                KSyncConflict kConflict = new KSyncConflict(kaction, "", e.Conflict); ;
                string des = "";

                if (tos != null)
                {
                    kaction = TOSSolveConfig(e);

                    if (kaction != KApplyAction.UserManual)
                    {
                        e.Action = (ApplyAction)(int)kaction;
                        kConflict.IsSolve = true;
                    }
                    else
                    {
                        DataView view = new DataView(e.Context.DataSet.Tables[tableName]);
                        view.RowStateFilter = DataViewRowState.CurrentRows | DataViewRowState.Deleted;
                        view.Sort = tos.GetSortExpression();
                        kConflict.ViewSort = view.Sort;

                        List<object> lstPriValue = new List<object>();
                        foreach (string node in tos.GetPrimaryColumns())
                        {
                            if (string.IsNullOrEmpty(node)) continue;
                            lstPriValue.Add(view[0][node]);
                        }
                        kConflict.KeyValues = lstPriValue.ToArray();
                        DataRowView[] selectedRow = view.FindRows(kConflict.KeyValues);
                        if (selectedRow.Length != 1)
                            throw new Exception(string.Format("Scope {0} Table {1} find with primary key but result {2}",
                                _SyncContext.CurrentScope.ScopeName, tos.TableName, selectedRow.Length));
                        selectedRow[0].Delete();
                        selectedRow[0].Row.AcceptChanges();
                    }
                }
                else
                {

                    des = string.Format(" TableOfScope Name={0} not found in CurrentScope ", tableName);
                    Log.Warn(des);
                }
                kConflict.Description = des;
                kConflict.Action = kaction;

                if (ConflictDatas.ContainsKey(tableName))
                {
                    ConflictDatas[tableName].Add(kConflict);
                }
                else
                {
                    List<KSyncConflict> lst = new List<KSyncConflict>();
                    lst.Add(kConflict);
                    ConflictDatas.Add(tableName, lst);
                }

            }
            catch (Exception ex)
            {
                Log.Fatal(string.Format("SessionId={0} {1} ApplyChangeFailed:",
                        e.Session.SessionId,
                        Context.CurrentScope.ScopeName)
                        , ex);
                throw ex;
            }


        }
        protected virtual void OnApplyingChanges(DbApplyingChangesEventArgs e)
        {
            try
            {
                Log.DebugFormat("SessionId={0} {1} ApplyingChanges event ", e.Session.SessionId, Context.CurrentScope.ScopeName);
                //MonitorEvent.Monitor.Info(string.Format("Cập nhật thay đổi Row:{0} Batch:{1}/{2}",e.Context.ScopeProgress.ro);
                if (e.Context.IsDataBatched)
                    MonitorEvent.Monitor.Info(string.Format("Cập nhật thay đổi({0}/{1})", SyncBatchInfo.Current, SyncBatchInfo.Total));
                else
                    MonitorEvent.Monitor.Info("Cập nhật thay đổi");

                if (SessionId.Equals(Guid.Empty))
                    SessionId = e.Session.SessionId;
                Transaction = e.Transaction as SqlTransaction;
                PreApplyTableCommand(e.Context.DataSet);

            }
            catch (Exception ex)
            {
                Log.Fatal(
                    string.Format("SessionId={0} {1} ApplyingChanges:",
                    e.Session.SessionId, Context.CurrentScope.ScopeName),
                        ex);
                throw ex;
            }
        }
        protected virtual void OnChangesApplied(DbChangesAppliedEventArgs e)
        {
            if ((e.Context.IsDataBatched && e.Context.IsLastBatch) || !e.Context.IsDataBatched)
            {
                ScopeProcess = e.Context.ScopeProgress.KClone();
                ResetSomeClientConfig();
            }
            Transaction = null;
        }
        protected virtual void OnSelectingChanges(DbSelectingChangesEventArgs e)
        {
            if (SessionId.Equals(Guid.Empty))
                SessionId = e.Session.SessionId;

            MonitorEvent.Monitor.Info("Lấy dữ liệu thay đổi");
            Transaction = (SqlTransaction)e.Transaction;
        }
        protected virtual void OnChangesSelected(DbChangesSelectedEventArgs e)
        {
            try
            {
                if ((!_SyncContext.CurrentScope.DownloadFirst && _SyncContext.Position == SyncProviderPosition.Remote&&_SyncContext.CurrentScope.SyncTimes==_SyncContext.CurrentScope.ConfigTimes)
                    || (!_SyncContext.CurrentScope.UploadFirst && _SyncContext.Position == SyncProviderPosition.Local && _SyncContext.CurrentScope.SyncTimes == _SyncContext.CurrentScope.ConfigTimes))
                {
                    e.Context.DataSet.Clear();
                    e.Context.DataSet.AcceptChanges();

                }
                else
                    RemoveConflictRowFromChange(e);
            }
            finally
            {
                Transaction = null;
            }
        }
        protected virtual void OnSyncProgress(DbSyncProgressEventArgs e)
        {
        }
        protected virtual void AttachProviderEvent()
        {
            if (_Provider != null)
            {
                _Provider.ApplyChangeFailed += new EventHandler<DbApplyChangeFailedEventArgs>(_Provider_ApplyChangeFailed);

                _Provider.ApplyingChanges += new EventHandler<DbApplyingChangesEventArgs>(_Provider_ApplyingChanges);
                _Provider.ChangesApplied += new EventHandler<DbChangesAppliedEventArgs>(_Provider_ChangesApplied);

                _Provider.SelectingChanges += new EventHandler<DbSelectingChangesEventArgs>(_Provider_SelectingChanges);
                _Provider.ChangesSelected += new EventHandler<DbChangesSelectedEventArgs>(_Provider_ChangesSelected);

                _Provider.SyncProgress += new EventHandler<DbSyncProgressEventArgs>(_Provider_SyncProgress);

                _Provider.BatchApplied += new EventHandler<DbBatchAppliedEventArgs>(_Provider_BatchApplied);
                _Provider.BatchSpooled += new EventHandler<DbBatchSpooledEventArgs>(_Provider_BatchSpooled);
            }
        }

        void _Provider_BatchSpooled(object sender, DbBatchSpooledEventArgs e)
        {

            MonitorEvent.Monitor.Info(string.Format("Lưu tệp batch({0}/{1})", e.CurrentBatchNumber, e.TotalBatchesSpooled));
        }

        void _Provider_BatchApplied(object sender, DbBatchAppliedEventArgs e)
        {
            MonitorEvent.Monitor.Info(string.Format("Cập nhật thay đổi({0}/{1} Batch)", e.CurrentBatchNumber, e.TotalBatchesToApply));
            SyncBatchInfo.Current++;
        }

        #endregion

        void _Provider_SyncProgress(object sender, DbSyncProgressEventArgs e)
        {
            OnSyncProgress(e);
        }

        void _Provider_ChangesSelected(object sender, DbChangesSelectedEventArgs e)
        {
            OnChangesSelected(e);

        }

        void _Provider_SelectingChanges(object sender, DbSelectingChangesEventArgs e)
        {
            OnSelectingChanges(e);
        }

        void _Provider_ChangesApplied(object sender, DbChangesAppliedEventArgs e)
        {
            OnChangesApplied(e);
        }

        void _Provider_ApplyingChanges(object sender, DbApplyingChangesEventArgs e)
        {
            OnApplyingChanges(e);
        }

        void _Provider_ApplyChangeFailed(object sender, DbApplyChangeFailedEventArgs e)
        {
            OnApplyChangeFailed(e);
        }

    }
}
